// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "net/http/http_auth.h"

#include <memory>
#include <set>
#include <string>

#include "base/memory/ref_counted.h"
#include "base/strings/string_util.h"
#include "net/base/net_errors.h"
#include "net/dns/mock_host_resolver.h"
#include "net/http/http_auth_challenge_tokenizer.h"
#include "net/http/http_auth_filter.h"
#include "net/http/http_auth_handler.h"
#include "net/http/http_auth_handler_factory.h"
#include "net/http/http_auth_handler_mock.h"
#include "net/http/http_auth_scheme.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_util.h"
#include "net/http/mock_allow_http_auth_preferences.h"
#include "net/ssl/ssl_info.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace net {

namespace {

    HttpAuthHandlerMock* CreateMockHandler(bool connection_based)
    {
        HttpAuthHandlerMock* auth_handler = new HttpAuthHandlerMock();
        auth_handler->set_connection_based(connection_based);
        std::string challenge_text = "Basic";
        HttpAuthChallengeTokenizer challenge(challenge_text.begin(),
            challenge_text.end());
        GURL origin("www.example.com");
        SSLInfo null_ssl_info;
        EXPECT_TRUE(auth_handler->InitFromChallenge(
            &challenge, HttpAuth::AUTH_SERVER, null_ssl_info, origin, BoundNetLog()));
        return auth_handler;
    }

    HttpResponseHeaders* HeadersFromResponseText(const std::string& response)
    {
        return new HttpResponseHeaders(
            HttpUtil::AssembleRawHeaders(response.c_str(), response.length()));
    }

    HttpAuth::AuthorizationResult HandleChallengeResponse(
        bool connection_based,
        const std::string& headers_text,
        std::string* challenge_used)
    {
        std::unique_ptr<HttpAuthHandlerMock> mock_handler(
            CreateMockHandler(connection_based));
        std::set<HttpAuth::Scheme> disabled_schemes;
        scoped_refptr<HttpResponseHeaders> headers(
            HeadersFromResponseText(headers_text));
        return HttpAuth::HandleChallengeResponse(mock_handler.get(), *headers,
            HttpAuth::AUTH_SERVER,
            disabled_schemes, challenge_used);
    }

} // namespace

TEST(HttpAuthTest, ChooseBestChallenge)
{
    static const struct {
        const char* headers;
        HttpAuth::Scheme challenge_scheme;
        const char* challenge_realm;
    } tests[] = {
        {
            // Basic is the only challenge type, pick it.
            "Y: Digest realm=\"X\", nonce=\"aaaaaaaaaa\"\n"
            "www-authenticate: Basic realm=\"BasicRealm\"\n",

            HttpAuth::AUTH_SCHEME_BASIC,
            "BasicRealm",
        },
        {
            // Fake is the only challenge type, but it is unsupported.
            "Y: Digest realm=\"FooBar\", nonce=\"aaaaaaaaaa\"\n"
            "www-authenticate: Fake realm=\"FooBar\"\n",

            HttpAuth::AUTH_SCHEME_MAX,
            "",
        },
        {
            // Pick Digest over Basic.
            "www-authenticate: Basic realm=\"FooBar\"\n"
            "www-authenticate: Fake realm=\"FooBar\"\n"
            "www-authenticate: nonce=\"aaaaaaaaaa\"\n"
            "www-authenticate: Digest realm=\"DigestRealm\", nonce=\"aaaaaaaaaa\"\n",

            HttpAuth::AUTH_SCHEME_DIGEST,
            "DigestRealm",
        },
        {
            // Handle an empty header correctly.
            "Y: Digest realm=\"X\", nonce=\"aaaaaaaaaa\"\n"
            "www-authenticate:\n",

            HttpAuth::AUTH_SCHEME_MAX,
            "",
        },
        {
            "WWW-Authenticate: Negotiate\n"
            "WWW-Authenticate: NTLM\n",

#if defined(USE_KERBEROS) && !defined(OS_ANDROID)
            // Choose Negotiate over NTLM on all platforms.
            // TODO(ahendrickson): This may be flaky on Linux and OSX as it
            // relies on being able to load one of the known .so files
            // for gssapi.
            HttpAuth::AUTH_SCHEME_NEGOTIATE,
#else
            // On systems that don't use Kerberos fall back to NTLM.
            HttpAuth::AUTH_SCHEME_NTLM,
#endif // defined(USE_KERBEROS)
            "",
        }
    };
    GURL origin("http://www.example.com");
    std::set<HttpAuth::Scheme> disabled_schemes;
    MockAllowHttpAuthPreferences http_auth_preferences;
    std::unique_ptr<HostResolver> host_resolver(new MockHostResolver());
    std::unique_ptr<HttpAuthHandlerRegistryFactory> http_auth_handler_factory(
        HttpAuthHandlerFactory::CreateDefault(host_resolver.get()));
    http_auth_handler_factory->SetHttpAuthPreferences(kNegotiateAuthScheme,
        &http_auth_preferences);

    for (size_t i = 0; i < arraysize(tests); ++i) {
        // Make a HttpResponseHeaders object.
        std::string headers_with_status_line("HTTP/1.1 401 Unauthorized\n");
        headers_with_status_line += tests[i].headers;
        scoped_refptr<HttpResponseHeaders> headers(
            HeadersFromResponseText(headers_with_status_line));

        SSLInfo null_ssl_info;
        std::unique_ptr<HttpAuthHandler> handler;
        HttpAuth::ChooseBestChallenge(http_auth_handler_factory.get(), *headers,
            null_ssl_info, HttpAuth::AUTH_SERVER, origin,
            disabled_schemes, BoundNetLog(), &handler);

        if (handler.get()) {
            EXPECT_EQ(tests[i].challenge_scheme, handler->auth_scheme());
            EXPECT_STREQ(tests[i].challenge_realm, handler->realm().c_str());
        } else {
            EXPECT_EQ(HttpAuth::AUTH_SCHEME_MAX, tests[i].challenge_scheme);
            EXPECT_STREQ("", tests[i].challenge_realm);
        }
    }
}

TEST(HttpAuthTest, HandleChallengeResponse)
{
    std::string challenge_used;
    const char* const kMockChallenge = "HTTP/1.1 401 Unauthorized\n"
                                       "WWW-Authenticate: Mock token_here\n";
    const char* const kBasicChallenge = "HTTP/1.1 401 Unauthorized\n"
                                        "WWW-Authenticate: Basic realm=\"happy\"\n";
    const char* const kMissingChallenge = "HTTP/1.1 401 Unauthorized\n";
    const char* const kEmptyChallenge = "HTTP/1.1 401 Unauthorized\n"
                                        "WWW-Authenticate: \n";
    const char* const kBasicAndMockChallenges = "HTTP/1.1 401 Unauthorized\n"
                                                "WWW-Authenticate: Basic realm=\"happy\"\n"
                                                "WWW-Authenticate: Mock token_here\n";
    const char* const kTwoMockChallenges = "HTTP/1.1 401 Unauthorized\n"
                                           "WWW-Authenticate: Mock token_a\n"
                                           "WWW-Authenticate: Mock token_b\n";

    // Request based schemes should treat any new challenges as rejections of the
    // previous authentication attempt. (There is a slight exception for digest
    // authentication and the stale parameter, but that is covered in the
    // http_auth_handler_digest_unittests).
    EXPECT_EQ(
        HttpAuth::AUTHORIZATION_RESULT_REJECT,
        HandleChallengeResponse(false, kMockChallenge, &challenge_used));
    EXPECT_EQ("Mock token_here", challenge_used);

    EXPECT_EQ(
        HttpAuth::AUTHORIZATION_RESULT_REJECT,
        HandleChallengeResponse(false, kBasicChallenge, &challenge_used));
    EXPECT_EQ("", challenge_used);

    EXPECT_EQ(
        HttpAuth::AUTHORIZATION_RESULT_REJECT,
        HandleChallengeResponse(false, kMissingChallenge, &challenge_used));
    EXPECT_EQ("", challenge_used);

    EXPECT_EQ(
        HttpAuth::AUTHORIZATION_RESULT_REJECT,
        HandleChallengeResponse(false, kEmptyChallenge, &challenge_used));
    EXPECT_EQ("", challenge_used);

    EXPECT_EQ(
        HttpAuth::AUTHORIZATION_RESULT_REJECT,
        HandleChallengeResponse(false, kBasicAndMockChallenges, &challenge_used));
    EXPECT_EQ("Mock token_here", challenge_used);

    EXPECT_EQ(
        HttpAuth::AUTHORIZATION_RESULT_REJECT,
        HandleChallengeResponse(false, kTwoMockChallenges, &challenge_used));
    EXPECT_EQ("Mock token_a", challenge_used);

    // Connection based schemes will treat new auth challenges for the same scheme
    // as acceptance (and continuance) of the current approach. If there are
    // no auth challenges for the same scheme, the response will be treated as
    // a rejection.
    EXPECT_EQ(
        HttpAuth::AUTHORIZATION_RESULT_ACCEPT,
        HandleChallengeResponse(true, kMockChallenge, &challenge_used));
    EXPECT_EQ("Mock token_here", challenge_used);

    EXPECT_EQ(
        HttpAuth::AUTHORIZATION_RESULT_REJECT,
        HandleChallengeResponse(true, kBasicChallenge, &challenge_used));
    EXPECT_EQ("", challenge_used);

    EXPECT_EQ(
        HttpAuth::AUTHORIZATION_RESULT_REJECT,
        HandleChallengeResponse(true, kMissingChallenge, &challenge_used));
    EXPECT_EQ("", challenge_used);

    EXPECT_EQ(
        HttpAuth::AUTHORIZATION_RESULT_REJECT,
        HandleChallengeResponse(true, kEmptyChallenge, &challenge_used));
    EXPECT_EQ("", challenge_used);

    EXPECT_EQ(
        HttpAuth::AUTHORIZATION_RESULT_ACCEPT,
        HandleChallengeResponse(true, kBasicAndMockChallenges, &challenge_used));
    EXPECT_EQ("Mock token_here", challenge_used);

    EXPECT_EQ(
        HttpAuth::AUTHORIZATION_RESULT_ACCEPT,
        HandleChallengeResponse(true, kTwoMockChallenges, &challenge_used));
    EXPECT_EQ("Mock token_a", challenge_used);
}

TEST(HttpAuthTest, GetChallengeHeaderName)
{
    std::string name;

    name = HttpAuth::GetChallengeHeaderName(HttpAuth::AUTH_SERVER);
    EXPECT_STREQ("WWW-Authenticate", name.c_str());

    name = HttpAuth::GetChallengeHeaderName(HttpAuth::AUTH_PROXY);
    EXPECT_STREQ("Proxy-Authenticate", name.c_str());
}

TEST(HttpAuthTest, GetAuthorizationHeaderName)
{
    std::string name;

    name = HttpAuth::GetAuthorizationHeaderName(HttpAuth::AUTH_SERVER);
    EXPECT_STREQ("Authorization", name.c_str());

    name = HttpAuth::GetAuthorizationHeaderName(HttpAuth::AUTH_PROXY);
    EXPECT_STREQ("Proxy-Authorization", name.c_str());
}

} // namespace net
